package com.glacier.common.core.utils;

import com.glacier.common.core.constant.ContentTypeEnum;
import com.glacier.common.core.domain.HttpRange;
import com.glacier.common.core.exception.BizException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URLEncoder;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;

/**
 * date 2022-11-05 16:13
 * 文件工具类
 *
 * @author glacier
 * @version 1.0
 */
public class FileUtil {

	private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class);

	/**
	 * .
	 */
	public static final String DOT = ".";

	private FileUtil() {
	}

	/**
	 * 向浏览器发送文件下载，支持断点续传
	 *
	 * @param file     要下载的文件
	 * @param request  请求对象
	 * @param response 响应对象
	 */
	public static void download(File file, HttpServletRequest request, HttpServletResponse response) {
		download(file, request, response, null);
	}

	/**
	 * 向浏览器发送文件下载，支持断点续传
	 *
	 * @param file     要下载的文件
	 * @param request  请求对象
	 * @param response 响应对象
	 * @param fileName 指定下载的文件名
	 */
	public static void download(File file, HttpServletRequest request, HttpServletResponse response, String fileName) {
		checkFile(file);
		// 通知客户端允许断点续传，响应格式为：Accept-Ranges: bytes
		response.setHeader("Accept-Ranges", "bytes");
		HttpRange httpRange = new HttpRange(request.getHeader("Range"), file.length())
				.parseRange();
		if (httpRange.isContinuable()) {
			response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
			// 针对 bytes=27000- 的请求
			response.setHeader("Content-Range", httpRange.buildContentRange());
		}
		try (WritableByteChannel outChannel = Channels.newChannel(response.getOutputStream());
			 RandomAccessFile raf = new RandomAccessFile(file, "r");
			 FileChannel inChannel = raf.getChannel()) {
			response.addHeader("Content-Disposition",
					"attachment; filename="
							+ URLEncoder.encode(defaultValue(fileName, file.getName()), StandardCharsets.UTF_8));
			// set the MIME type.
			response.setContentType(ContentTypeEnum.findContentType(getFileSuffix(file.getName())));
			response.addHeader("Content-Length", String.valueOf(httpRange.getContentLength()));
			long transfer;
			long readLength = httpRange.getPastLength();
			long total = 0L;
			while (readLength < httpRange.getToLength()) {
				transfer = inChannel.transferTo(readLength, httpRange.getContentLength(), outChannel);
				if (transfer <= 0) {
					break;
				}
				total += transfer;
				readLength += transfer;
			}
			LOGGER.debug("下载完成！发送：{}", total);
		} catch (FileNotFoundException e) {
			throw new BizException("500", "文件不存在！");
		} catch (IOException e) {
			/*
			 * 在写数据的时候， 对于 ClientAbortException 之类的异常，
			 * 是因为客户端取消了下载，而服务器端继续向浏览器写入数据时， 抛出这个异常，这个是正常的。
			 * 尤其是对于迅雷这种吸血的客户端软件， 明明已经有一个线程在读取 bytes=1275856879-1275877358，
			 * 如果短时间内没有读取完毕，迅雷会再启第二个、第三个。。。线程来读取相同的字节段， 直到有一个线程读取完毕，迅雷会 KILL
			 * 掉其他正在下载同一字节段的线程， 强行中止字节读出，造成服务器抛 ClientAbortException。
			 * 所以，我们忽略这种异常
			 */
			LOGGER.warn("提醒：向客户端传输时出现IO异常，但此异常是允许的，有可能客户端取消了下载，导致此异常，不用关心!", e);
		}
	}

	/**
	 * 获取文件后缀名
	 *
	 * @param fileName 文件名
	 * @return
	 */
	public static String getFileSuffix(String fileName) {
		int index;
		if (isBlank(fileName) ||
				(index = fileName.lastIndexOf(DOT)) < 0) {
			return ContentTypeEnum.DEFAULT.getContentType();
		}
		return fileName.substring(index + 1)
				.toLowerCase();
	}

	/**
	 * 检查文件
	 *
	 * @param file 文件
	 */
	private static void checkFile(File file) {
		if (file == null || !file.exists()) {
			throw new BizException("500", "文件已丢失或不存在");
		}
		if (!file.isFile()) {
			throw new BizException("500", "该文件是一个文件夹");
		}
		if (file.length() <= 0) {
			throw new BizException("500", "该文件是一个空文件");
		}
		if (!file.canRead()) {
			throw new BizException("500", "该文件没有读取权限");
		}
	}

	/**
	 * 判断字符串是否为空
	 *
	 * @param str
	 * @return
	 */
	private static boolean isBlank(String str) {
		return str == null || str.isBlank();
	}

	/**
	 * 设置默认值
	 *
	 * @param input
	 * @param defaultValue
	 * @return
	 */
	private static String defaultValue(String input, String defaultValue) {
		return isBlank(input) ? defaultValue : input;
	}
}
